// 13.2 拷贝控制和资源管理
/**
 * 通常，管理类外资源的类必须定义拷贝控制成员。一旦一个类需要析构函数，那么它几乎肯定也需要一个拷贝构造函数和一个拷贝赋值运算符。
 * 类的行为像一个值，意味着它应该也有自己的状态。当我们拷贝一个像值的对象时，副本和原对象是完全独立的。改变副本不会对原对象有任何影响，反之亦然。
 * 行为像指针的类则共享状态。当我们拷贝一个这种类的对象时，副本和原对象使用相同的底层数据。改变副本也会改变原对象，反之亦然。
 * 为了定义这些成员，我们首先必须确定此类型对象的拷贝语义。一般来说，有两种选择：
 * 1. 可以定义拷贝操作，使类的行为看起来像一个值（副本和原对象完全独立）。
 * 2. 可以定义拷贝操作，使类的行为看起来像一个指针（副本和原对象使用相同的底层数据）。
 * 通常，类直接拷贝内置类型（不包括指针）成员；这些成员本身就是值，因此通常应该让它们的行为像值一样。我们如何拷贝指针成员决定了像HasPtr这样的类是具有类值行为还是类指针行为。
 */

#include <iterator>
#include <vector>
#include <list>
#include <deque>
#include <forward_list>
#include <string>
#include <array>
#include <stack>
#include <queue>
#include <algorithm>
#include <numeric>
using std::swap;
using std::vector, std::list, std::deque, std::forward_list, std::string, std::array, std::stack, std::queue;
#include "../Chapter07/Sales_data.h"
#include <iostream>
using std::begin, std::cbegin, std::end, std::cend, std::find, std::accumulate, std::equal, std::fill, std::fill_n, std::back_inserter;
using std::cin, std::cout, std::endl;
using std::copy, std::replace, std::replace_copy;
#include <map>
#include <set>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <memory>
#include <new>
using namespace std;

// 类值版本的HasPtr
class HasPtr
{
public:
    HasPtr(const string &s = string()) : ps(new string(s)), i(0) {}
    HasPtr(const HasPtr &p) : ps(new string(*p.ps)), i(p.i) {} // 对ps指向的string，每个HasPtr对象都有自己的拷贝
    HasPtr &operator=(const HasPtr &);                         // 声明赋值运算符，但还未定义
    ~HasPtr() { delete ps; }

private:
    string *ps;
    int i;
};
// 定义赋值运算符
HasPtr &HasPtr::operator=(const HasPtr &rhs)
{
    auto newp = new string(*rhs.ps); // 拷贝底层string
    delete ps;                       // 释放旧内存
    ps = newp;                       // 从右侧运算对象拷贝数据到本对象
    i = rhs.i;
    return *this;
}

class HasPtr2
{
public:
    // 构造函数分配的string和新的计数器，将计数器置为1
    HasPtr2(const string &s = string()) : ps(new string(s)), i(0), use(new size_t(1)) {}
    // 拷贝构造函数拷贝所有三个成员，并递增计数器
    HasPtr2(const HasPtr2 &p) : ps(p.ps), i(p.i), use(p.use) { ++*use; }
    HasPtr2 &operator=(const HasPtr2 &); // 声明赋值运算符，但还未定义
    ~HasPtr2(); // 声明析构函数

private:
    string *ps;
    int i;
    size_t *use; // 用来记录有多少个对象共享*ps的成员
};
// 定义析构函数
HasPtr2::~HasPtr2()
{
    if(--*use == 0) // 如果引用计数变为0
    {
        delete ps;  // 释放string内存
        delete use; // 释放计数器内存
    }
}
// 定义拷贝赋值运算符
HasPtr2& HasPtr2::operator=(const HasPtr2 &rhs)
{
    ++*rhs.use; // 递增右侧运算对象的引用计数
    if(--*use == 0) // 然后递减本对象的引用计数
    {
        delete ps;  // 如果没有其他用户
        delete use; // 释放本对象分配的成员
    }
    ps = rhs.ps;  // 将数据从rhs拷贝到本对象
    i = rhs.i;
    use = rhs.use;
    return *this; // 返回本对象
}

int main()
{
    // 13.2.1 行为像值的类
    // 为了提供类值的行为，对于类管理的资源，每个对象都应该拥有一份自己的拷贝。
    HasPtr hp;

    // 值类拷贝赋值运算符
    // 赋值运算符通常组合了析构函数和构造函数的操作。类似析构函数，赋值操作会销毁左侧运算对象的资源。类似拷贝构造函数，赋值操作会从右侧运算对象拷贝数据。
    {
        HasPtr thp("hello"); // 值类版本的HasPtr
        hp = thp;
    }

    // 关键概念：赋值运算符
    // 当你编写赋值运算符时，有两点需要记住：
    // 1. · 如果将一个对象赋予它自身，赋值运算符必须能正确工作。
    // 2. · 大多数赋值运算符组合了析构函数和拷贝构造函数的工作。
    // 当你编写一个赋值运算符时，一个好的模式是先将右侧运算对象拷贝到一个局部临时对象中。当拷贝完成后，销毁左侧运算对象的现有成员就是安全的了。
    // 一旦左侧运算对象的资源被销毁，就只剩下将数据从临时对象拷贝到左侧运算对象的成员中了。
    // 对于一个赋值运算符来说，正确工作是非常重要的，即使是将一个对象赋予它自身，也要能正确工作。一个好的方法是在销毁左侧运算对象资源之前拷贝右侧运算对象。

    // 13.2.2 定义行为像指针的类
    // 对于行为类似指针的类，我们需要为其定义拷贝构造函数和拷贝赋值运算符，来拷贝指针成员本身而不是它指向的string。
    // 令一个类展现类似指针的行为的最好方法是使用shared_ptr来管理类中的资源。
    // 拷贝（或赋值）一个shared_ptr会拷贝（赋值）shared_ptr所指向的指针。shared_ptr类自己记录有多少用户共享它所指向的对象。当没有用户使用对象时，shared_ptr类负责释放资源。
    // 但是，有时我们希望直接管理资源。在这种情况下，使用引用计数（referencecount）（参见12.1.1节，第402页）就很有用了。
    // 为了说明引用计数如何工作，我们将重新定义HasPtr，令其行为像指针一样，但我们不使用shared_ptr，而是设计自己的引用计数。

    // 引用计数
    // 引用计数的工作方式如下：
    // 1. · 除了初始化对象外，每个构造函数（拷贝构造函数除外）还要创建一个引用计数，用来记录有多少对象与正在创建的对象共享状态。
    //      当我们创建一个对象时，只有一个对象共享状态，因此将计数器初始化为1。
    // 2. · 拷贝构造函数不分配新的计数器，而是拷贝给定对象的数据成员，包括计数器。
    //      拷贝构造函数递增共享的计数器，指出给定对象的状态又被一个新用户所共享。
    // 3. · 析构函数递减计数器，指出共享状态的用户少了一个。如果计数器变为0，则析构函数释放状态。
    // 4. · 拷贝赋值运算符递增右侧运算对象的计数器，递减左侧运算对象的计数器。
    //      如果左侧运算对象的计数器变为0，意味着它的共享状态没有用户了，拷贝赋值运算符就必须销毁状态。
    // 当拷贝或赋值对象时，我们拷贝指向计数器的指针。使用这种方法，副本和原对象都会指向相同的计数器。

    // 定义一个引用计数的类
    // 通过使用引用计数，我们就可以编写类指针的HasPtr版本了：[插图]


    return 0;
}